Visual Basic 6 comes with two new common controls for dealing with dates: The MonthView control and the DateTimePicker control. The former is a calendarlike control, and the latter is a text box control for entering dates and times. The two are closely related in that the DateTimePicker control uses a MonthView control when the user drops down a calendar for selecting a date.
After you place a MonthView control on a form, you can right-click on it to display its custom Property Pages dialog box, shown in Figure 11-4. The Value property is the date highlighted in the control. (By the way, click on the Down arrow to the right of the Value field to get a taste of what a DateTimePicker control is.) MinDate and MaxDate set the range of valid dates that can be selected in the MonthView control; the StartOfWeek property determines the weekday that appears in the leftmost column in the calendar.
Figure 11-4. Setting design-time properties of a MonthView control.
A number of Boolean properties affect the appearance and the behavior of the control. If ShowWeekNumbers is True, the MonthView control displays the number of weeks elapsed since the beginning of the year. If MultiSelect is True, the user is allowed to select a range of dates: In this case, the maximum number of consecutive days that can be selected is equal to the value of the MaxSelCount property. (The default is one week.) The ShowToday property lets you decide whether the Today legend should be displayed.
The MonthView control can display up to 12 months, and the number of displayed months is the product of the properties MonthRows and MonthColumns. By default, when the user clicks on the arrow buttons the control scrolls a number of months equal to the months displayed in the control, but you can modify this behavior by assigning a nonzero value to the ScrollRate property.
The MonthView control exposes many properties that are related to foreground and background colors, and it's easy to confuse them. Refer to Figure 11-5 to understand how you can use the ForeColor, TitleForeColor, TitleBackColor, MonthBackColor, and TrailingForeColor properties. (Trailing days are those days that belong to previous or next months.) The MonthBackColor property also affects the color of weekday names and numbers. Oddly, the control also exposes the standard BackColor property, but it doesn't appear to have any other effect than coloring a line of pixels near the bottom and right borders.
Figure 11-5. You can modify the colors used by individual areas in a MonthView control.
Among the many design-time properties, you might notice in particular the DataSource, DataField, DataMember, and DataFormat properties. In fact, MonthView is a data-aware control that can be bound to any Date field exposed by a standard Data control, a RemoteData control, or any ADO data source.
CAUTION
If you're going to localize your application to other languages, you'll be glad to know that the MonthView control automatically adapts itself to the user's locale and correctly translates all the month and day names. There's only a minor bug in this implementation: The Today legend isn't localized, so you should set the ShowToday property to False and provide a legend elsewhere on the form.
Users can act on the MonthView control in several ways, a few of which aren't immediately apparent. Most people can easily figure out that users can move to the next or previous month by clicking on one of the two arrows near the control's title and that they can select a date simply by clicking on it. Some users will even figure out that they can select a range of dates (if MultiSelect is True) by clicking on a date while pressing the Shift key. I doubt, however, that many users guess that a click on the month's name in the control's title displays a pop-up menu that lets them move to any month in the current year. Even more counter-intuitive is that a click on the year number displays two spin buttons that can take a user to any year, future or past. (See Figure 11-6.) Don't forget to mention these hidden features in your program's documentation, or even better, show their usage in a Label control on the same form as the MonthView control.
Figure 11-6. The demonstration program lets you experiment with all the advanced features of the MonthView control. The spin buttons in the title area appear if you click on the year number.
Unless you need to perform special operations, using the MonthView control in code is straightforward. The control exposes the Value property, which you can assign to highlight a given date or read to retrieve the day selected by the user. You don't even need to extract the day, month, or year portions from the Value property because the control exposes also the Day, Month, and Year properties. Conveniently, these properties can be assigned too—for example, you can programmatically display the next month using this code:
If MonthView1.Month < 12 Then MonthView1.Month = MonthView1.Month + 1 Else MonthView1.Month = 1 MonthView1.Year = MonthView1.Year + 1 End If |
The DayOfWeek property returns the weekday number of the selected date. Also, this property is writable, so, for example, you can highlight Monday in the current week using the following single statement:
MonthView1.DayOfWeek = vbMonday |
Be aware, however, that Day, Month, Year, and DayOfWeek properties can't be assigned if MultiSelect is True.
CAUTION
While experimenting with the MonthView control, I've discovered an unexpected behavior: If the control has the focus and you click on another control on the same form, the other control gets the focus but not the Click event. This might puzzle your users, much as it confused me when I realized that if the focus is on a MonthView control, a click on push buttons doesn't yield the expected results. This bug will probably be fixed in a future service pack. In the meantime, the workaround to this problem is really cumbersome, to say the least, and relies on the MouseDown event instead of the Click event:
Dim MousePressed As Boolean ' A form-level variable Private Sub cmdTryMe_MouseDown(Button As Integer, _ Shift As Integer, X As Single, Y As Single) MousePressed = True Call DoSomething End Sub Private Sub cmdTryMe_MouseUp(Button As Integer, _ Shift As Integer, X As Single, Y As Single) MousePressed = False End Sub Private Sub cmdTryMe_Click() ' This event might be called as a response to a hot key ' or a click when the focus isn't on the MonthView control. If Not MousePressed Then Call DoSomething End Sub Private Sub DoSomething() ' The code that must execute when the button is clicked MsgBox "Button has been clicked!" End Sub |
You can set the MinDate and MaxDate property to limit the range of date values that the user can select. If the MultiSelect property is True, you can select a number of consecutive dates. You retrieve the selected range using the SelStart and SelEnd properties. (These properties return Date values.) The maximum number of days in the selected range depends on the value of the MaxSelCount property.
Each time the user selects a new date, a custom SelChange event fires. This event receives the start date and end date of the selected range and enables the programmer to cancel the operation. For example, you can refuse a selection that includes a weekend day:
Private Sub MonthView1_SelChange(ByVal StartDate As Date, _ ByVal EndDate As Date, Cancel As Boolean) Dim d As Date ' A Date variable can be used to control a For loop. For d = StartDate To EndDate If Weekday(d) = vbSunday Or Weekday(d) = vbSaturday Then ' Cancel the selection if the day is Sunday or Saturday. Cancel = True Exit For End If Next End Sub |
CAUTION
The MonthView control suffers from a bug. Unless the user has selected three or more dates, setting the Cancel parameter to True doesn't actually cancel the operation. This bug will probably be fixed in future versions of the control, but currently there isn't any simple workaround for it. (I'm currently using version 6.00.8177 of the MsComCt2.ocx file.)
Two other custom events, DateClick and DateDblClick, fire when the user selects a new date. When a user clicks on a date, your Visual Basic application receives a SelChange event and then a DateClick event. If a date is double-clicked, your code receives SelChange, DateClick, DateDblClick, and DateClick events, in this order, so you should account for the fact that a double-click also fires two DateClick events. Both events receive one argument, the date being clicked or double-clicked:
Private Sub MonthView1_DateDblClick(ByVal DateDblClicked As Date) Dim descr As String descr = InputBox("Enter a description for day " & _ FormatDateTime(DateDblClicked, vbLongDate)) If Len(descr) Then ' Save the description (omitted) ... ' ... End If End Sub |
The MonthView control gives you several ways, apart from its many color properties, to affect the control's appearance. For example, you can show up to 12 months in the control by assigning suitable values to the MonthRows and MonthColumns properties. Changing these properties at run time, however, can cause a problem in that you have no control over a MonthView's size (which depends on the number of months displayed, the font used, the presence of a border, and other settings). To help you determine the best values for the MonthRows and MonthColumns properties, the MonthView control supports the ComputeControlSize method. This method takes as arguments the number of rows and columns and returns the computed width and height of the corresponding MonthView control in its third and fourth argument.
' Evaluate the size of a MonthView control with 2 rows and 3 columns Dim wi As Single, he As Single MonthView1.ComputeControlSize 2, 3, wi, he |
The ComputeControlSize method comes in handy when you want to display the highest number of months in a form. The following routine has been extracted from the demonstration program provided on the companion CD:
Private Sub cmdTile_Click() ' Find the best value for MonthRows and MonthColumns. Dim rows As Integer, cols As Integer Dim wi As Single, he As Single For rows = 6 To 1 Step -1 ' Note how we avoid creating more than 12 months. For cols = 12 \ rows To 1 Step -1 MonthView1.ComputeControlSize rows, cols, wi, he If wi <= ScaleWidth _ MonthView1.Left And _ he < ScaleHeight _ MonthView1.Top Then MonthView1.MonthRows = rows MonthView1.MonthColumns = cols Exit Sub End If Next Next End Sub |
The MonthView control lets the programmer draw attention to dates in the calendar by displaying them with a bold attribute. You could use this feature whenever the contents of the control changes by writing code in the GetDayBold event procedure, as in the following example:
' Display all Sundays and major holidays in boldface. Sub MonthView1_GetDayBold(ByVal StartDate As Date, _ ByVal Count As Integer, State() As Boolean) Dim i As Long, d As Date d = StartDate For i = 0 To Count - 1 If Weekday(d) = vbSunday Then State(i) = True ' Mark all Sundays. ElseIf Month(d) = 12 And Day(d) = 25 Then State(i) = True ' Xmas time. Else ' Deal here with other holidays... End If d = d + 1 Next End Sub |
The GetDayBold event receives three parameters: StartDate is a Date value that corresponds to the first day displayed in the control (this includes trailing days, which are the days that belong to the previous month), Count is the number of visible days, and State is a zero-based Boolean array with Count elements. Thus, to enforce a bold attribute to a given date, you only have to assign True to the corresponding item in the State array.
Alternatively, you can modify the bold attribute for any date that's currently displayed in the control by writing code outside the GetDayBold event procedure. You do this using VisibleDays and DayBold properties. The VisibleDays property accepts an index in the range from 1 to the number of visible days and returns the Date value that corresponds to that day. The problem with this property is that there's no easy way to know in advance how many days are visible in the control and therefore what the highest value for the index is. The Visual Basic documentation incorrectly states that the index must be in the range from 1 through 42, but this doesn't take into account the MonthView control's ability to display multiple months. The simplest way to deal with this issue is to set up an error handler, as in the following code:
Dim tmpDate As Date ' Exit the loop when the index isn't valid any longer. On Error GoTo EndTheLoop For i = 1 To 366 ' Visit each day. tmpDate = MonthView1.VisibleDays(i) Next EndTheLoop: ' Get here when the index becomes invalid. |
The VisibleDays property returns a Date value whose fractional portion is equal to the current time on your system. This undocumented behavior can get in the way when you compare the returned value to a Date constant or variable.
The DayBold property takes as an argument a Date value that corresponds to a visible day and sets or returns the bold attribute for that day. This property lets you mark a number of days at the same time even if you aren't processing a GetDayBold event. You typically use the DayBold property together with the VisibleDays property, as in the following piece of code:
Private Sub cmdMark_Click() Dim i As Integer On Error GoTo EndOfLoop For i = 1 To 999 ' Mark all Fridays. If Weekday(MonthView1.VisibleDays(i)) = vbFriday Then MonthView1.DayBold(MonthView1.VisibleDays(i)) = True End If Next EndOfLoop: End Sub |
The MonthView control is an ideal source for drag-and-drop operations because it permits you to copy a date value to any other control that accepts a string through this mechanism. The key for a correct implementation of drag-and-drop is the HitTest method, the syntax of which is the following:
Area = MonthView1.HitTest(X, Y, HitDate) |
Area is an integer that indicates which area of the control the x and y coordinates correspond to. (See the Visual Basic documentation or the demonstration program's source code on the companion CD for a list of all possible return values.) When the function returns the value 4-mvwCalendarDay, the HitDate variable is assigned the Date value of the day in the calendar at x and y coordinates. With this method, you'll find it easy to implement an effective drag-and-drop routine. The following code is taken from the demonstration program shown in Figure 11-6:
' Start a drag-and-drop operation. Private Sub MonthView1_MouseDown(Button As Integer, Shift As Integer, _ X As Single, Y As Single) ' Exit if the right button isn't clicked. If Button <> vbRightButton Then Exit Sub ' Exit if mouse isn't over a valid date. If MonthView1.HitTest(X, Y, DraggedDate) <> mvwCalendarDay Then Exit Sub End If ' Now DraggedDate contains the date to be dragged, ' and we can start the drag operation. MonthView1.OLEDrag End Sub Private Sub MonthView1_OLEStartDrag(Data As MSComCtl2.DataObject, _ AllowedEffects As Long) ' When this event fires, DraggedDate contains a valid date. Data.SetData Format(DraggedDate, "long date") AllowedEffects = vbDropEffectCopy End Sub |
The preceding code assumes that the OLEDropMode property of the control over which the mouse button is released is set to the value 2-Automatic.